// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdio.h>
#include <string.h>
#include <zircon/hw/gpt.h>

#include <efi/protocol/block-io.h>
#include <efi/protocol/device-path-to-text.h>
#include <efi/protocol/device-path.h>
#include <efi/protocol/disk-io.h>
#include <efi/protocol/loaded-image.h>

#include "osboot.h"

static bool path_node_match(efi_device_path_protocol* a, efi_device_path_protocol* b) {
  size_t alen = a->Length[0] | (a->Length[1] << 8);
  size_t blen = b->Length[0] | (b->Length[1] << 8);
  if (alen != blen) {
    return false;
  }
  if (memcmp(a, b, alen)) {
    return false;
  }
  return true;
}

static efi_device_path_protocol* path_node_next(efi_device_path_protocol* node) {
  if (node->Type == DEVICE_PATH_END) {
    return NULL;
  }
  return ((void*)node) + (node->Length[0] | (node->Length[1] << 8));
}

static bool path_prefix_match(efi_device_path_protocol* path, efi_device_path_protocol* prefix) {
  if ((path == NULL) || (prefix == NULL)) {
    return false;
  }
  for (;;) {
    if (prefix->Type == DEVICE_PATH_END) {
      return true;
    }
    if (!path_node_match(path, prefix)) {
      return false;
    }
    if ((path = path_node_next(path)) == NULL) {
      return false;
    }
    prefix = path_node_next(prefix);
  }
}

static void print_path(efi_boot_services* bs, efi_device_path_protocol* path) {
  efi_device_path_to_text_protocol* ptt;
  efi_status status = bs->LocateProtocol(&DevicePathToTextProtocol, NULL, (void**)&ptt);
  if (status != EFI_SUCCESS) {
    printf("<cannot print path>");
    return;
  }
  char16_t* txt = ptt->ConvertDevicePathToText(path, false, false);
  if (txt == NULL) {
    printf("<cannot print path>");
    return;
  }
  puts16(txt);
  printf("\n");
  bs->FreePool(txt);
}

typedef struct {
  efi_disk_io_protocol* io;
  efi_handle h;
  efi_boot_services* bs;
  efi_handle img;
  uint64_t first;
  uint64_t last;
  uint32_t blksz;
  uint32_t id;
} disk_t;

static efi_status disk_read(disk_t* disk, size_t offset, void* data, size_t length) {
  uint64_t size = (disk->last - disk->first) * disk->blksz;

  if ((offset > size) || ((size - offset) < length)) {
    return EFI_INVALID_PARAMETER;
  }

  return disk->io->ReadDisk(disk->io, disk->id, (disk->first * disk->blksz) + offset, length, data);
}

static void disk_close(disk_t* disk) {
  disk->bs->CloseProtocol(disk->h, &DiskIoProtocol, disk->img, NULL);
}

static int disk_find_boot(efi_handle img, efi_system_table* sys, bool verbose, disk_t* disk) {
  bool found = false;
  efi_boot_services* bs = sys->BootServices;
  efi_handle* list;
  size_t count;
  efi_status status;
  efi_loaded_image_protocol* li;

  status = bs->OpenProtocol(img, &LoadedImageProtocol, (void**)&li, img, NULL,
                            EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
  if (status != EFI_SUCCESS) {
    return -1;
  }

  efi_device_path_protocol* imgdevpath;
  status = bs->OpenProtocol(li->DeviceHandle, &DevicePathProtocol, (void**)&imgdevpath, img, NULL,
                            EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
  if (status != EFI_SUCCESS) {
    goto fail_open_devpath;
  }

  if (verbose) {
    printf("BootLoader Path: ");
    print_path(bs, li->FilePath);
    printf("BootLoader Device: ");
    print_path(bs, imgdevpath);
  }

  status = bs->LocateHandleBuffer(ByProtocol, &BlockIoProtocol, NULL, &count, &list);
  if (status != EFI_SUCCESS) {
    printf("find_boot_disk() - no block io devices found\n");
    goto fail_get_list;
  }

  for (size_t n = 0; n < count; n++) {
    efi_block_io_protocol* bio;
    status = bs->OpenProtocol(list[n], &BlockIoProtocol, (void**)&bio, img, NULL,
                              EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
    if (status != EFI_SUCCESS) {
      continue;
    }

    efi_device_path_protocol* path;
    status = bs->OpenProtocol(list[n], &DevicePathProtocol, (void**)&path, img, NULL,
                              EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
    if (status != EFI_SUCCESS) {
      bs->CloseProtocol(list[n], &BlockIoProtocol, img, NULL);
      continue;
    }

    bool match = false;

    // if a non-logical partition, check for match
    if (!bio->Media->LogicalPartition && bio->Media->MediaPresent) {
      match = path_prefix_match(imgdevpath, path);
    }

    if (verbose) {
      printf("BlockIO Device: ");
      print_path(bs, path);
      printf("              : #%zu, %zuMB%s%s%s%s%s%s\n", n,
             bio->Media->LastBlock * bio->Media->BlockSize / 1024 / 1024,
             bio->Media->RemovableMedia ? " Removable" : "",
             bio->Media->MediaPresent ? " Present" : "",
             bio->Media->LogicalPartition ? " Logical" : "", bio->Media->ReadOnly ? " RO" : "",
             bio->Media->WriteCaching ? " WC" : "", match ? " BootDevice" : "");
    }

    if (match && !found) {
      status = bs->OpenProtocol(list[n], &DiskIoProtocol, (void**)&disk->io, img, NULL,
                                EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
      if (status != EFI_SUCCESS) {
        printf("find_boot_disk() - cannot get disk io protocol\n");
      } else {
        disk->first = 0;
        disk->last = bio->Media->LastBlock;
        disk->id = bio->Media->MediaId;
        disk->blksz = bio->Media->BlockSize;
        disk->h = list[n];
        disk->img = img;
        disk->bs = bs;
        found = true;
      }
    }

    bs->CloseProtocol(list[n], &BlockIoProtocol, img, NULL);
    bs->CloseProtocol(list[n], &DevicePathProtocol, img, NULL);
  }

  bs->FreePool(list);

fail_get_list:
  bs->CloseProtocol(li->DeviceHandle, &DevicePathProtocol, img, NULL);

fail_open_devpath:
  bs->CloseProtocol(img, &LoadedImageProtocol, img, NULL);

  return found ? 0 : -1;
}

static int disk_find_kernel(disk_t* disk, bool verbose, const uint8_t* guid_value,
                            const char* guid_name) {
  gpt_header_t gpt;
  efi_status status = disk_read(disk, disk->blksz, &gpt, sizeof(gpt));
  if (status != EFI_SUCCESS) {
    return -1;
  }

  if (gpt.magic != GPT_MAGIC) {
    printf("gpt - bad magic!\n");
    return -1;
  }

  if (verbose) {
    printf("gpt: size:    %u\n", gpt.size);
    printf("gpt: current: %zu\n", gpt.current);
    printf("gpt: backup:  %zu\n", gpt.backup);
    printf("gpt: first:   %zu\n", gpt.first);
    printf("gpt: last:    %zu\n", gpt.last);
    printf("gpt: entries: %zu\n", gpt.entries);
    printf("gpt: e.count: %u\n", gpt.entries_count);
    printf("gpt: e.size:  %u\n", gpt.entries_size);
  }

  if ((gpt.magic != GPT_MAGIC) || (gpt.size != GPT_HEADER_SIZE) ||
      (gpt.entries_size != GPT_ENTRY_SIZE) || (gpt.entries_count > 256)) {
    printf("gpt - malformed header\n");
    return -1;
  }

  gpt_entry_t* table;
  size_t tsize = gpt.entries_count * gpt.entries_size;

  status = disk->bs->AllocatePool(EfiLoaderData, tsize, (void**)&table);
  if (status != EFI_SUCCESS) {
    printf("gpt - allocation failure\n");
    return -1;
  }

  status = disk_read(disk, disk->blksz * gpt.entries, table, tsize);
  if (status != EFI_SUCCESS) {
    disk->bs->FreePool(table);
    printf("gpt - io error\n");
    return -1;
  }

  bool found = false;
  for (unsigned n = 0; n < gpt.entries_count; n++) {
    if ((table[n].first == 0) || (table[n].last == 0) || (table[n].last < table[n].first)) {
      // ignore empty or bogus entries
      continue;
    }

    const char* type;
    if (!memcmp(table[n].type, guid_value, GPT_GUID_LEN)) {
      type = guid_name;
      disk->first = table[n].first;
      disk->last = table[n].last;
      found = true;
    } else {
      type = "unknown";
    }

    if (verbose) {
      char name[GPT_NAME_LEN / 2];
      for (unsigned i = 0; i < GPT_NAME_LEN / 2; i++) {
        unsigned c = table[n].name[i * 2 + 0] | (table[n].name[i * 2 + 1] << 8);
        if ((c != 0) && ((c < ' ') || (c > 127))) {
          c = '.';
        }
        name[i] = c;
      }
      name[GPT_NAME_LEN / 2 - 1] = 0;
      printf("#%03d %zu..%zu %zx name='%s' type='%s'\n", n, table[n].first, table[n].last,
             table[n].flags, name, type);
    }
  }
  disk->bs->FreePool(table);

  return found ? 0 : -1;
}

void* image_load_from_disk(efi_handle img, efi_system_table* sys, size_t* _sz,
                           const uint8_t* guid_value, const char* guid_name) {
  static bool verbose = false;
  static uint8_t sector[512];
  efi_boot_services* bs = sys->BootServices;
  disk_t disk;

  if (disk_find_boot(img, sys, verbose, &disk) < 0) {
    printf("Cannot find bootloader disk.\n");
    return NULL;
  }

  if (disk_find_kernel(&disk, verbose, guid_value, guid_name)) {
    printf("Cannot find %s partition on bootloader disk.\n", guid_name);
    goto fail0;
  }

  efi_status status = disk_read(&disk, 0, sector, 512);
  if (status != EFI_SUCCESS) {
    goto fail0;
  }

  size_t sz = image_getsize(sector, 512);
  if (sz == 0) {
    printf("%s partition has no valid header\n", guid_name);
    goto fail0;
  }

  size_t pages = (sz + 4095) / 4096;
  void* image;
  status = bs->AllocatePages(AllocateAnyPages, EfiLoaderData, pages, (efi_physical_addr*)&image);
  if (status != EFI_SUCCESS) {
    printf("Failed to allocate %zu bytes to load %s image\n", sz, guid_name);
    goto fail0;
  }

  status = disk_read(&disk, 0, image, sz);
  if (status != EFI_SUCCESS) {
    printf("Failed to read image from %s partition\n", guid_name);
    goto fail1;
  }

  if (identify_image(image, sz) != IMAGE_COMBO) {
    printf("%s partition has no valid image\n", guid_name);
    goto fail1;
  }

  *_sz = sz;
  return image;

fail1:
  bs->FreePages((efi_physical_addr)image, pages);
fail0:
  disk_close(&disk);
  return NULL;
}
